/*
 * 
 * the interface to call Weka
 * 
 * Created on March 26, 2010, 7:45 PM
 * Copyright (C) 2010 HIT-CIR
 */
package nlpeap.ml.weka;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.List;

import nlpeap.config.ConfigProperties;
import nlpeap.ml.FeatureDescription;
import nlpeap.ml.FeatureType;
import nlpeap.ml.Instance;
import nlpeap.ml.OfflineClassifier;

import weka.core.Instances;
import weka.core.Utils;
import weka.classifiers.Classifier;
import weka.core.converters.ConverterUtils.DataSource;

/**
 * Call the classifiers in Weka
 * 
 * @author chuter
 */
public class WEKAInstanceClassifier implements OfflineClassifier {
	private final static double _threshold = 
		ConfigProperties.getInstance().getClassifyThreshold();
	private final Classifier classifier;
	private final Instances dataset;
	private List<FeatureDescription> _fds;
	private static String _modelName = "model";

	/**
	 * <p>
	 * Constructor that sets the classifier and the training date by loading
	 * from the the specific file
	 * </p>
	 * 
	 * @param filename
	 *            the name of the file
	 * @throws IOException
	 * @throws ClassNotFoundException
	 */
	public WEKAInstanceClassifier(String filename) throws IOException,
			ClassNotFoundException {
		ObjectInputStream f = new ObjectInputStream(new FileInputStream(
				filename));
		classifier = (Classifier) f.readObject();
		dataset = (Instances) f.readObject();
	}

	/**
	 * <p>
	 * Constructor that sets the classifier and the training date by loading
	 * from the the specific file
	 * </p>
	 * 
	 * @param file
	 *            the file object
	 * @throws IOException
	 * @throws ClassNotFoundException
	 */
	public WEKAInstanceClassifier(File file) throws IOException,
			ClassNotFoundException {
		ObjectInputStream f = new ObjectInputStream(new FileInputStream(file));
		classifier = (Classifier) f.readObject();
		dataset = (Instances) f.readObject();
	}

	public static void setModleName(String model_name) { _modelName = model_name; }
	
	/**
	 * <p>
	 * Set the header of the data set, it is a list of features in fact, it
	 * describes each feature of each instance in the data set, including the
	 * feature type and the possible values
	 * </p>
	 * 
	 * @param fds
	 *            the FeatureDescription List
	 * @see FeatureDescription
	 */
	public void setHeader(List<FeatureDescription> fds) {
		_fds = fds;
	}

	/**
	 * Load the data set for Weka, the file type can be cvs and arff.<br>
	 * And it sets the last feature as the class type of the instance
	 * 
	 * @param fileName
	 *            the File Name
	 * @return Instances the instances of data set
	 * @throws Exception
	 */
	public Instances loadInstances(String fileName) throws Exception {
		DataSource source = new DataSource(fileName);
		Instances data = source.getDataSet();
		if (data.classIndex() == -1)
			data.setClassIndex(data.numAttributes() - 1);
		return data;
	}

	/**
	 * Run a classifier to train a model
	 * 
	 * @param modelName		the model name what to be saved
	 * @param classifier	the classifier name of Weka like weka.classifiers.trees.J48
	 * @param options		the options for the specific classifier
	 * @throws IOException
	 */
	public static void runLearner(String trainData_file, String classifier,
		String options) throws IOException {
		//construct the training data set and set the last feature as the class type
		System.out.println(trainData_file);
		Instances trainingSet = new Instances(new FileReader(
        		new File(trainData_file)));
		trainingSet.setClassIndex(trainingSet.numAttributes() - 1);
		
		Classifier cls;
		try {
			//if cause errors when call the given classifier then call J48
			//which is named weka.classifiers.trees.J48
			try {
				Class.forName(classifier);
			} catch (ClassNotFoundException e) {
				System.err.format("Class %s not found, using J48\n", classifier);
				classifier = "weka.classifiers.trees.J48";
			}
			String[] args = Utils.splitOptions(options);
			cls = Classifier.forName(classifier, args);
			cls.buildClassifier(trainingSet);
		} catch (Exception e) {
			throw new RuntimeException("Weka learner failed", e);
		}
		ObjectOutputStream oos = new ObjectOutputStream(
				new FileOutputStream(new File(_modelName)));
		oos.writeObject(cls);
		//delete the training instances, just leave the header to check whether the test
		//instance is valid 
		trainingSet.delete(); 
		oos.writeObject(trainingSet);
	}

	/**
	 * set each feature value of the instance  according to their Descriptions
	 * 
	 * @param weka_ins		the Instance object for Weka
	 * @param test_ins		the test Instance of the specific problem
	 * @see Instance
	 */
	private void setInstanceFS(weka.core.Instance weka_ins, Instance test_ins) {
		for (int i = 0; i < _fds.size() - 1; i++) {
			FeatureDescription fd = _fds.get(i);

			if (fd.type == FeatureType.FT_SCALAR)
				weka_ins.setValue(i, ((Number) test_ins.getFeature(fd))
						.doubleValue());
			else
				weka_ins.setValue(i, test_ins.getFeature(fd).toString());
		}
	}
	
	/**
	 * classify the instances that extends Instance, and add the results
	 * to the result List
	 * 
	 * @param problems		instances of the specific problem
	 * @param output		the result List
	 * @see Instance
	 */
	public void classify(List<? extends Instance> problems, List output) {
		for (Instance test_ins : problems) {
			double retVal = singleInstanceScore(test_ins);
			FeatureDescription des = _fds.get(_fds.size() - 1);

			switch (des.type) {
			case FT_BOOL:
				if (retVal < _threshold)
					output.add(Boolean.FALSE);
				else
					output.add(Boolean.TRUE);
				break;
			case FT_NOMINAL_ENUM:
				output.add(des.cls.getEnumConstants()[(int) retVal]);
				break;
			case FT_SCALAR:
				output.add(retVal);
				break;
			default:
				System.out.println(des.type);
				throw new RuntimeException("Feature Type Unknown");
			}
			
		}
	}
	
	/** get the score for each <i>Instance</i> by particular 
	 * 	<i>Classifier</i> */
	public double singleInstanceScore(Instance inst)
	{
		//create an new instance with the same number of features and reference to
		//the training data set
		weka.core.Instance weka_ins = new weka.core.Instance(dataset.numAttributes());
		weka_ins.setDataset(dataset);

		//set the feature values of the instance for Weka
		setInstanceFS(weka_ins, inst);
		try {
			return classifier.classifyInstance(weka_ins);
		} catch (Exception e) {
			e.printStackTrace();
			throw new RuntimeException("Error", e);
		}

	}

	@Override
	public String getName() 
	{ return OfflineClassifier.WEKA; }

}
